﻿using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace NumberParser
{
    public static class Parser
    {
        #region Constants

        private const string NUMBER_REGEX = @"\A((?<radix>[0-9]+)\@)?(?<negative>-)?(?<wholeNumber>[0-9A-Za-z]+)(\.(?<fractionNumber>[0-9A-Za-z]+))?(\#(?<exponent>[0-9A-Za-z]+))?\Z";

        #endregion

        #region Variables

        private static Regex _numberMatcher;

        #endregion

        #region Constructors

        static Parser()
        {
            _numberMatcher = new Regex(NUMBER_REGEX);
        }

        #endregion

        public static bool TryParse(string text, out double value)
        {
            try
            {
                value = Parse(text);
                return true;
            }
            catch (Exception)
            {
                value = 0;
                return false;
            }
        }

        public static double Parse(string text)
        {
            var match = _numberMatcher.Match(text);
            Require.Argument(() => match).IsValid(x => x.Success, "The input text does not describe a number.");

            return Parse(radix: match.Groups["radix"].Success ? (int)Parse(10, false, match.Groups["radix"].Value, null, null) : 10,
                         isNegative: match.Groups["negative"].Success,
                         wholeNumberText: match.Groups["wholeNumber"].Value,
                         fractionNumberText: match.Groups["fractionNumber"].Value,
                         exponentText: match.Groups["exponent"].Value);
        }

        private static double Parse(int radix, bool isNegative, string wholeNumberText, string fractionNumberText, string exponentText)
        {
            var value = ParseWholeNumberText(radix, wholeNumberText);
            value += ParseFractionNumberText(radix, fractionNumberText);
            value = ParseExponentText(radix, exponentText, value);
            value = ParseIsNegative(isNegative, value);
            return value;
        }

        private static double ParseWholeNumberText(int radix, string wholeNumberText)
        {
            Require.Argument(() => wholeNumberText).IsNotNullOrEmpty();

            return Enumerable
                    .Range(0, wholeNumberText.Length)
                    .Select(index => new {
                                            Exponent = wholeNumberText.Length.Subtract(index).Subtract(1),
                                            Value = ParseCharacterIntoDigit(radix, wholeNumberText[index])
                                         } as dynamic)
                    .Sum(x => (double)(x.Value * radix.Pow((double)x.Exponent)));
        }

        private static double ParseFractionNumberText(int radix, string fractionNumberText)
        {
            return string.IsNullOrEmpty(fractionNumberText)
                ? 0
                : Enumerable
                    .Range(0, fractionNumberText.Length)
                    .Select(index => new
                    {
                        Exponent = fractionNumberText.Length.Subtract(index).Subtract(1),
                        Value = ParseCharacterIntoDigit(radix, fractionNumberText[index])
                    } as dynamic)
                    .Sum(x => (double)(x.Value * radix.Pow((double)x.Exponent))) / radix.Pow(fractionNumberText.Length);
        }

        private static double ParseExponentText(int radix, string exponentText, double value)
        {
            return string.IsNullOrEmpty(exponentText) ? value : value * radix.Pow(Parse(radix, false, exponentText, null, null));
        }

        private static double ParseIsNegative(bool isNegative, double value)
        {
            return isNegative ? -value : value;
        }

        /// <remarks>
        /// 10 digits + 26 letters, case-insensitive.
        /// A different system would be needed for base-64 encoding.
        /// </remarks>
        private static int ParseCharacterIntoDigit(int radix, char ch)
        {
            Require.Argument(() => radix).IsInRange(1, 37);
            Require.Argument(() => ch).IsLetterOrDigit();
            var value = char.IsDigit(ch) ? ch.Subtract('0') : 10.Add(char.ToUpper(ch).Subtract('A'));
            Require.Argument(() => value).IsInRange(0, radix);
            return value;
        }
    }
}